﻿
using EmperialApps.WeatherSpark.Data;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;

namespace EmperialApps.WeatherSpark.Internal {

    /// <summary>Represents a source for forecasts.</summary>
    internal abstract class ForecastSource {

        /// <summary>Gets the name of the container for the source.</summary>
        public ForecastSourceId Id { get { return _id; } }

        /// <summary>Gets the name of the source.</summary>
        public string Name { get { return _name; } }

        /// <summary>Gets the detail level supported by the source.</summary>
        public abstract ForecastDisplayLevel SupportedDisplayLevel { get; }

        /// <summary>Gets the source URL for the specified location.</summary>
        public abstract void GetSourceUrl<T>( Place place, T state, Action<T, string> receiveSourceUrl );

        /// <summary>Gets the URL of the source page showing the specified forecast.</summary>
        public abstract IEnumerable<Pair<string, Uri>> GetPageUrls( Coordinate location, Units units, string sourceUrl );


        /// <summary>Gets identifiers for all available forecast sources.</summary>
        public static IEnumerable<ForecastSourceId> Supported {
            get {
                yield return ForecastSourceId.NOAA;
                yield return ForecastSourceId.YRNO;
            }
        }

        /// <summary>Returns the forecast source with the specified identifier.</summary>
        public static ForecastSource FromId( ForecastSourceId id ) {
            switch( id ) {
                case ForecastSourceId.NOAA:
                    return NoaaSource.Instance;
                case ForecastSourceId.YRNO:
                    return YrnoSource.Instance;

                default:
                    return null;
            }
        }


        /// <summary>Gets the latest place corrections.</summary>
        public static Place.Corrections GetCorrections( ) {
            if( _corrections.LoadCount == 0 ) {
                _corrections = Place.Corrections.Load( _corrections.Save( ) );

                // Try loading the latest corrections.
                typeof( ForecastSource ).Log( "Updating corrections" );
                var client = new WebClient( );
                client.OpenReadCompleted += OnCorrectionsOpenReadCompleted;
                client.OpenReadAsync( new Uri( Settings.Corrections ) );
            }

            return _corrections;
        }


        /// <inheritdoc/>
        public sealed override string ToString( ) {
            return this._name;
        }


        #region Private Members

        // Ensure corrections are up to date.
        private static Place.Corrections _corrections = GetCorrections( );

        private readonly ForecastSourceId _id;
        private readonly string _name;

        private ForecastSource( ForecastSourceId id, string name = null ) {
            this._id = id;
            this._name = name ?? this._id.ToString( );
        }


        private static void OnCorrectionsOpenReadCompleted( object sender, OpenReadCompletedEventArgs e ) {
            if( e.WasCancelled( ) ) {
                typeof( ForecastSource ).Log( "Corrections update cancelled" );
                return;
            }

            if( e.Error != null ) {
                typeof( ForecastSource ).Log( "Corrections update failed: " + e.Error );
            }
            else {
                try {
                    // Read corrections from stream.
                    using( var reader = new System.IO.StreamReader( e.Result ) ) {
                        string corrections = reader.ReadToEnd( );
                        _corrections = Place.Corrections.Load( corrections );
                        typeof( ForecastSource ).Log( "Updated corrections: " + _corrections.LoadCount );
                    }
                }
                catch( Exception ex ) {
                    typeof( ForecastSource ).Log( "Could not read corrections update: " + ex );
                }
            }
        }


        private sealed class NoaaSource : ForecastSource {
            public static readonly NoaaSource Instance = new NoaaSource( );

            private NoaaSource( ) : base( ForecastSourceId.NOAA ) { }

            public override ForecastDisplayLevel SupportedDisplayLevel {
                get { return ForecastDisplayLevel.DetailsMask & ~ForecastDisplayLevel.PressureMask; }
            }

            public override void GetSourceUrl<T>( Place place, T state, Action<T, string> receiveSourceUrl ) {
                string sourceUrl = GetWeatherUrl( place.Location ) + "&FcstType=digitalDWML";
                receiveSourceUrl( state, sourceUrl );
            }

            public override IEnumerable<Pair<string, Uri>> GetPageUrls( Coordinate location, Units units, string sourceUrl ) {
                string unit = units == Units.Metric ? "1" : "0";
                string pageUrl = GetWeatherUrl( location ) + "&unit=" + unit;
                string mobileUrl = pageUrl.Replace( "forecast", "mobile" ).Replace( "MapClick", "index" );
                yield return Pair.Create( "Forecast", new Uri( pageUrl, UriKind.Absolute ) );
                yield return Pair.Create( "Mobile", new Uri( mobileUrl, UriKind.Absolute ) );
            }

            private static string GetWeatherUrl( Coordinate location ) {
                return string.Format(
                    CultureInfo.InvariantCulture,
                    "http://forecast.weather.gov/MapClick.php?lat={0:0.00}&lon={1:0.00}",
                    location.Latitude, location.Longitude );
            }
        }

        private sealed class YrnoSource : ForecastSource {
            public static readonly YrnoSource Instance = new YrnoSource( );

            private YrnoSource( ) : base( ForecastSourceId.YRNO ) { }

            public override ForecastDisplayLevel SupportedDisplayLevel {
                get { return ForecastDisplayLevel.DetailsMask & ~ForecastDisplayLevel.HumidityMask; }
            }

            public override void GetSourceUrl<T>( Place place, T state, Action<T, string> receiveSourceUrl ) {
                // If place does not contain a YRNO link, look up nearest location.
                string sourceUrl = place.Link;
                if( !IsYrnoAddress( sourceUrl ) )
                    FindNearestSourceUrl( place.Location, state, receiveSourceUrl );
                // Otherwise, return appropriate link.
                else if( sourceUrl.EndsWith( "/" ) )
                    receiveSourceUrl( state, sourceUrl + "forecast.xml" );
                else
                    receiveSourceUrl( state, sourceUrl );
            }

            public override IEnumerable<Pair<string, Uri>> GetPageUrls( Coordinate location, Units units, string sourceUrl ) {
                string pageUrl = sourceUrl.Replace( "forecast.xml", "" );
                yield return Pair.Create( "Forecast", new Uri( pageUrl, UriKind.Absolute ) );
            }

            private static bool IsYrnoAddress( string sourceUrl ) {
                return sourceUrl != null
                    && sourceUrl.StartsWith( Place.YrnoAddress );
            }

            private static void FindNearestSourceUrl<T>( Coordinate location, T state, Action<T, string> receiveSourceUrl ) {
                typeof( YrnoSource ).Log( "Search for YRNO location near " + location );
                Uri address = new Uri( string.Format( CultureInfo.InvariantCulture, @"http://api.geonames.org/findNearbyPlaceName?username=weatherspark&style=FULL&lat={0}&lng={1}", location.Latitude, location.Longitude ) );
                var userToken = Pair.Create( state, receiveSourceUrl );

                var client = new WebClient( );
                client.OpenReadCompleted += OnSearchComplete<T>;
                client.OpenReadAsync( address, userToken );
            }

            private static void OnSearchComplete<T>( object sender, OpenReadCompletedEventArgs e ) {
                T state;
                Action<T, string> receiveSourceUrl;
                Pair.TryGetValues( e.UserState, out state, out receiveSourceUrl );

                Place[] places;
                try {
                    using( e.Result )
                        places = Place.Load( Units.Metric, e.Result, GetCorrections( ) );
                }
                catch( Exception ex ) {
                    typeof( YrnoSource ).Log( "Location search failed: " + ex );
                    return;
                }

                Place place = places.ElementAtOrDefault( 0 );
                if( IsYrnoAddress( place.Link ) ) {
                    typeof( YrnoSource ).Log( "Found YRNO source URL: " + place.Link );
                    Instance.GetSourceUrl( place, state, receiveSourceUrl );
                }
                else {
                    typeof( YrnoSource ).Log( "Could not find YRNO source URL." );
                    receiveSourceUrl( state, "-Could not find YRNO source URL." );
                }
            }
        }

        #endregion

    }

}
